package com.sxpi.zixun.service.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sxpi.zixun.common.enums.AdminUserResultEnum;
import com.sxpi.zixun.common.ex.PugValidatorException;
import com.sxpi.zixun.common.utils.date.TmDateUtil;
import com.sxpi.zixun.common.utils.fn.asserts.Vsserts;
import com.sxpi.zixun.config.redis.AdminRedisKeyManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 问题1：为什么这里不使用工具类
 *      工具类 static方法，方便进行操作和获取。不利于后续的管理
 * 问题2：为什么要让Spring容器管理
 *      因为要考虑把这里面的常量用配置文件管理，甚至用统一配置中心
 * @Author: 董伟豪
 * @Date: 2022/6/13 9:48
 */
@Slf4j
@Component
public class JwtService implements AdminRedisKeyManager {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Value("${pug.jwt.key}")
    private static final String KEY = "pugadmin123456";

    /**
     * 这里前端也必须要放,可以看作成是一种暗号
     */
    @Value("${pug.jwt.prefix}")
    private static final String PUG_TOKEN_PREFIX = "pugbear_";

    /**
     * 指定作者
     */
    @Value("${pug.jwt.author}")
    private static final String AUTHOR = "sxpi";

    /**
     * token的过期时间 单位毫秒
     */
    private static final Long ONE_SECOND = 1000L;
    private static final Long TOKEN_EXPIRE_TIME = ONE_SECOND * 60 * 30;

    /**
     * 这个是我判断token的需要需要续期的时间
     */
    private static final Long TOKEN_RENEWAL = TOKEN_EXPIRE_TIME - 10 * -1;

    /**
     * jwt 和 redis的双倍续期
     */
    private static final Long TOKEN_REDIS_RENEWAL = TOKEN_EXPIRE_TIME * 2;

    /**
     * 创建token
     * @param userId
     * @return
     */
    public String createToken(Long userId){
        // 1：确定token加密签名的算法和密钥
        Algorithm algorithm = Algorithm.HMAC256(KEY);
        // 2: 创建token
        String token = JWT.create()
                // 指定作者（其实也可以不用写也行）
                .withIssuer(AUTHOR)
                // 指定用户id即可，不要去放完整的用户对象信息。因为生成token太长，
                // 为什么就放一个id，因为后续开发我们会把解析的用户id，去db或redis查一遍。保证实时性。
                // 方便以后对平台的一些恶意分子直接拉黑， 就会生效。
                .withClaim(PUG_USER_ID, userId)
                // 指定token的过期时间
                .withExpiresAt(new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME))
                // 签发时间
                .withIssuedAt(new Date())
                // 签名返回
                .sign(algorithm);
        return token;
    }

    /**
     * 校验token
     * @return
     */
    public boolean verify(String token){
        try {
            // 1：确定token加密签名的算法和密钥
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 2 : 获取token的校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(AUTHOR)
                    .build(); //Reusable verifier instance
            // 3: 开始校验，如果校验通过DecodedJWT.如果token是伪造或者失效的，就会出现异常。
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception ex) {
            System.out.println("token失效了");
            return false;
        }
    }

    /**
     * 根据token获取用户的id
     * @param token
     * @return
     */
    public Long getTokenUserId(String token) {
        try {
            // 1：确定token加密签名的算法和密钥
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 2 : 获取token的校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(AUTHOR)
                    .build(); //Reusable verifier instance
            // 3: 开始校验，如果校验通过DecodedJWT.如果token是伪造或者失效的，就会出现异常。
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaim(PUG_USER_ID).asLong();
        } catch (Exception ex) {
            throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR);
        }
    }

    /**
     * 根据token获取签发时间
     * @param token
     * @return
     */
    public Date getTokenIssuedTime(String token){
        try {
            // 1：确定token加密签名的算法和密钥
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 2 : 获取token的校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(AUTHOR)
                    .build(); //Reusable verifier instance
            // 3: 开始校验，如果校验通过DecodedJWT.如果token是伪造或者失效的，就会出现异常。
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getIssuedAt();
        } catch (Exception ex) {
            throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR);
        }
    }

    /**
     * 获取过期时间
     * @return
     */
    public Date getTokenExpireTime(String token){
        try {
            // 1：确定token加密签名的算法和密钥
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 2 : 获取token的校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(AUTHOR)
                    .build(); //Reusable verifier instance
            // 3: 开始校验，如果校验通过DecodedJWT.如果token是伪造或者失效的，就会出现异常。
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getExpiresAt();
        } catch (Exception ex) {
            throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR);
        }
    }


    /**
     * 校验token 自动续期
     * @param token
     * @param userId
     * @return
     */
    public boolean refreshTokenRedis(String token, String userId, HttpServletResponse response){
        String tokenKey = USER_LOGIN_TOKEN_KEY + token;
        String cacheKeyToken = redisTemplate.opsForValue().get(tokenKey);
        if(Vsserts.isEmpty(cacheKeyToken)){
            return false;
        }
        try {
            // 1：确定token加密签名的算法和密钥
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 2 : 获取token的校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(AUTHOR)
                    .build(); //Reusable verifier instance
            // 3: 开始校验，如果校验通过DecodedJWT.如果token是伪造或者失效的，就会出现异常。
            verifier.verify(token);
        }catch (TokenExpiredException e){
            // 如果过期了，Redis还能找到，说明还可以继续激活使用 token大于1800秒小于3600秒
            if(redisTemplate.hasKey(tokenKey)){
                // 生成新的token
                String newToken = createToken(new Long(userId));
                redisTemplate.opsForValue().set(tokenKey,newToken,TOKEN_REDIS_RENEWAL, TimeUnit.MILLISECONDS);
                return true;
            }
        }catch (Exception e){
            throw new PugValidatorException(AdminUserResultEnum.TOKEN_NOT_FOUND);
        }
        return true;
    }

    @Deprecated
    private void refreshToken(String token,Long tokenUserId,HttpServletResponse response){
        // token的续期
        Date tokenIssuedTime = this.getTokenIssuedTime(token);
        Date tokenExpireTime = this.getTokenExpireTime(token);
        log.info("签发时间：" + TmDateUtil.dateToString(tokenIssuedTime,"yyyy-MM-dd HH:mm:ss"));
        log.info("当前时间：" + TmDateUtil.dateToString(new Date(),"yyyy-MM-dd HH:mm:ss"));
        log.info("过期时间：" + TmDateUtil.dateToString(tokenExpireTime,"yyyy-MM-dd HH:mm:ss"));
        // 用当前时间减去签发时间，如果 > 20
        int diffminutes = TmDateUtil.diffminutes(tokenIssuedTime,new Date());
        // token的存活时间 如果大于 我想让token续期的时间，那就true
        if(diffminutes >= TOKEN_RENEWAL){
            // 续期，重新生成一个新的token，可以通过response的头部输出token，然后前台通过response获取
            String newToken = this.createToken(tokenUserId);
            response.setHeader(RESPONSE_AUTH_TOKEN,newToken);
        }
    }

    /**
     * 只要currentDate < tokenIssuedTime的时间，就不会去续期
     * 也就是currentDate绝对会一直增大 一直增大，绝对有一天大于 tokenIssuedTime 的时间
     * @param token
     * @param tokenUserId
     * @param response
     * @return
     */
    public void refreshToken2(String token,Long tokenUserId,HttpServletResponse response){
        // token的续期
        Date tokenIssuedTime = this.getTokenIssuedTime(token);
        // 这个值会一直增大 增大
        Date currentDate = TmDateUtil.addMinute(new Date(),TOKEN_RENEWAL.intValue());
        log.info("签发时间：" + TmDateUtil.dateToString(tokenIssuedTime,"yyyy-MM-dd HH:mm:ss"));
        log.info("刷新时间：" + TmDateUtil.dateToString(currentDate,"yyyy-MM-dd HH:mm:ss"));
        System.out.println(currentDate.after(tokenIssuedTime));
        // 如果刷新时间在签发时间的后面，就直接续签
        if(currentDate.after(tokenIssuedTime)){
            // 续期，重新生成一个新的token，可以通过response的头部输出token，然后前台通过response获取
            String newToken = this.createToken(tokenUserId);
            response.setHeader(RESPONSE_AUTH_TOKEN,newToken);
        }
    }

    /**
     * 获取请求头的用户id
     * @param request
     * @return
     */
    public String getTokenUserId(HttpServletRequest request){
        String token_userid = request.getHeader(TOKEN_USER_ID);
        if(Vsserts.isEmpty(token_userid)){
            return null;
        }
        return token_userid;
    }

    /**
     * 过期时间续期
     *
     * @param token
     * @param tokenUserId
     * @param response
     */
    @Deprecated
    private void refreshToken3(String token, Long tokenUserId, HttpServletResponse response) {
        // token续期
        // 获取token的签发时间 --------第一种写法
        Date expireTime = this.getTokenExpireTime(token);
        // 假设过期时间是 30分钟，用过期时间减去当前时间：30 29 28 27 20...10
        int diffminutes = TmDateUtil.diffminutes(new Date(),expireTime);
        // 如果时间以及过去了20分钟，到最后十分钟的时候就开始续期 10 9 8 7
        if(diffminutes <= TOKEN_RENEWAL){
            // 续期，重新生成一个新的token
            String newToken = this.createToken(tokenUserId);
            // 通过response的头部输出token,然后前台通过reponse获取
            response.setHeader(RESPONSE_AUTH_TOKEN, newToken);
        }
    }

    /**
     * 获取请求头中的token
     * @param request
     * @return
     */
    public String getToken(HttpServletRequest request){
        String token = request.getHeader(TOKEN_NAME);
        if(Vsserts.isEmpty(token)){
            return null;
        }
        if(!token.startsWith(PUG_TOKEN_PREFIX)){
            throw new PugValidatorException(AdminUserResultEnum.TOKEN_NOT_FOUND);
        }
        // 截取前缀
        token = token.substring(PUG_TOKEN_PREFIX.length());
        return token;
    }

    /**
     * 登录使用，双倍时间
     * @param token
     */
    public void redisToken(String token){
        // Jwt和redis的续期双倍时间
        String tokenKey = USER_LOGIN_TOKEN_KEY + token;
        // 记住，在redis的单位默认是 秒，也就是说这个tokenkey是双倍，时间是30分钟，双倍也就是60分钟  转换成秒 3600秒
        redisTemplate.opsForValue().set(tokenKey, token, TOKEN_REDIS_RENEWAL, TimeUnit.MILLISECONDS);
    }

}
